cmake_minimum_required(VERSION 3.10)
cmake_policy(SET CMP0043 OLD) # testing the old behavior

project(Preprocess)

# This test is meant both as a test and as a reference for supported
# syntax on native tool command lines.

# Determine the build tool being used.  Not all characters can be
# escaped for all build tools.  This test checks all characters known
# to work with each tool and documents those known to not work.
if("${CMAKE_GENERATOR}" MATCHES "Xcode")
  set(PP_XCODE 1)
endif()
if("${CMAKE_GENERATOR}" MATCHES "Unix Makefiles")
  set(PP_UMAKE 1)
endif()
if("${CMAKE_GENERATOR}" MATCHES "NMake Makefiles")
  set(PP_NMAKE 1)
endif()
if("${CMAKE_GENERATOR}" MATCHES "MinGW Makefiles")
  set(PP_MINGW 1)
endif()
if("${CMAKE_GENERATOR}" MATCHES "Borland Makefiles")
  set(PP_BORLAND 1)
endif()
if("${CMAKE_GENERATOR}" MATCHES "Watcom WMake")
  set(PP_WATCOM 1)
endif()
if("${CMAKE_GENERATOR}" MATCHES "Visual Studio")
  set(PP_VS 1)
endif()
if(CMAKE_C_COMPILER_ID STREQUAL "Clang" AND
   "x${CMAKE_C_SIMULATE_ID}" STREQUAL "xMSVC")
   set(CLANG_MSVC_WINDOWS 1)
endif()
if(CLANG_MSVC_WINDOWS AND
   "x${CMAKE_C_COMPILER_FRONTEND_VARIANT}" STREQUAL "xGNU")
   set(CLANG_GNULIKE_WINDOWS 1)
endif()

# Some tests below check the PP_* variables set above.  They are meant
# to test the case that the build tool is at fault.  Other tests below
# check the compiler that will be used when the compiler is at fault
# (does not work even from a command shell).

#-----------------------------------------------------------------------------
# Construct a C-string literal to test passing through a definition on
# the command line.  We configure the value into a header so it can be
# checked in the executable at runtime.  The semicolon is handled
# specially because it needs to be escaped in the COMPILE_DEFINITIONS
# property value to avoid separating definitions but the string value
# must not have it escaped inside the configured header.
set(STRING_EXTRA "")

if(NOT BORLAND)
  # Borland: ;
  # The Borland compiler will simply not accept a non-escaped semicolon
  # on the command line.  If it is escaped \; then the escape character
  # shows up in the preprocessing output too.
  set(SEMICOLON "\;")
endif()

string(APPEND STRING_EXTRA " ")

if(NOT PP_BORLAND AND NOT PP_WATCOM AND NOT CLANG_GNULIKE_WINDOWS)
  # Borland, WMake: multiple spaces
  # The make tool seems to remove extra whitespace from inside
  # quoted strings when passing to the compiler.  It does not have
  # trouble passing to other tools, and the compiler may be directly
  # invoked from the command line.
  string(APPEND STRING_EXTRA " ")
endif()

if(NOT PP_VS)
  # VS: ,
  # Visual Studio will not accept a comma in the value of a definition.
  # The comma-separated list of PreprocessorDefinitions in the project
  # file seems to be parsed before the content of entries is examined.
  string(APPEND STRING_EXTRA ",")
endif()

if(NOT PP_MINGW AND NOT CLANG_GNULIKE_WINDOWS)
  # MinGW: &
  # When inside -D"FOO=\"a & b\"" MinGW make wants -D"FOO=\"a "&" b\""
  # but it does not like quoted ampersand elsewhere.
  string(APPEND STRING_EXTRA "&")
endif()

if(NOT PP_MINGW AND NOT CLANG_GNULIKE_WINDOWS)
  # MinGW: |
  # When inside -D"FOO=\"a | b\"" MinGW make wants -D"FOO=\"a "|" b\""
  # but it does not like quoted pipe elsewhere.
  string(APPEND STRING_EXTRA "|")
endif()

if(NOT PP_BORLAND AND NOT PP_MINGW AND NOT PP_NMAKE)
  # Borland, NMake, MinGW: ^
  # When inside -D"FOO=\"a ^ b\"" the make tools want -D"FOO=\"a "^" b\""
  # but do not like quoted carrot elsewhere.  In NMake the non-quoted
  # syntax works when the flags are not in a make variable.
  string(APPEND STRING_EXTRA "^")
endif()

if(NOT PP_BORLAND AND NOT PP_MINGW AND NOT PP_NMAKE)
  # Borland, MinGW: < >
  # Angle-brackets have funny behavior that is hard to escape.
  string(APPEND STRING_EXTRA "<>")
endif()

set(EXPR_OP1 "/")
if((NOT MSVC OR PP_NMAKE) AND
   NOT CMAKE_C_COMPILER_ID STREQUAL "Intel" AND
   NOT CLANG_MSVC_WINDOWS)
  # MSVC cl, Intel icl: %
  # When the cl compiler is invoked from the command line then % must
  # be written %% (to distinguish from %ENV% syntax).  However cl does
  # not seem to accept the syntax when it is invoked from inside a
  # make tool (nmake, mingw32-make, etc.).  Instead the argument must
  # be placed inside a response file.  Then cl accepts it because it
  # parses the response file as it would the normal windows command
  # line.  Currently only NMake supports running cl with a response
  # file.  Supporting other make tools would require CMake to generate
  # response files explicitly for each object file.
  #
  # When the icl compiler is invoked from the command line then % must
  # be written just '%'.  However nmake requires '%%' except when using
  # response files.  Currently we have no way to affect escaping based
  # on whether flags go in a response file, so we just have to skip it.
  string(APPEND STRING_EXTRA "%")
  set(EXPR_OP1 "%")
endif()

# XL: )(
# The XL compiler cannot pass unbalanced parens correctly to a tool
# it launches internally.
if(CMAKE_C_COMPILER_ID STREQUAL "XL")
  string(APPEND STRING_EXTRA "()")
else()
  string(APPEND STRING_EXTRA ")(")
endif()

# General: \"
# Make tools do not reliably accept \\\" syntax:
#  - MinGW and MSYS make tools crash with \\\"
#  - Borland make actually wants a mis-matched quote \\"
#    or $(BACKSLASH)\" where BACKSLASH is a variable set to \\
#  - VS IDE gets confused about the bounds of the definition value \\\"
#  - NMake is okay with just \\\"
#  - The XL compiler does not re-escape \\\" when launching an
#    internal tool to do preprocessing .
#  - The IntelLLVM C and C++ compiler drivers do not re-escape the \\\" when
#    launching the underlying compiler. FIXME: this bug is expected to be fixed
#    in a future release.
if((PP_NMAKE OR PP_UMAKE) AND
    NOT CMAKE_C_COMPILER_ID STREQUAL "XL" AND
    NOT CMAKE_C_COMPILER_ID STREQUAL "IntelLLVM" AND
    NOT CMAKE_CXX_COMPILER_ID STREQUAL "IntelLLVM")
  string(APPEND STRING_EXTRA "\\\"")
endif()

# General: #
# MSVC will not accept a # in the value of a string definition on the
# command line.  The character seems to be simply replaced by an
# equals =.  According to "cl -help" definitions may be specified by
# -DMACRO#VALUE as well as -DMACRO=VALUE.  It must be implemented by a
# simple search-and-replace.
#
# The Borland compiler will parse both # and \# as just # but the make
# tool seems to want \# sometimes and not others.
#
# Unix make does not like # in variable settings without extra
# escaping.  This could probably be fixed but since MSVC does not
# support it and it is not an operator it is not worthwhile.

# Compose the final test string.
set(STRING_VALUE "hello`~!@$*_+-=}{][:'.?/${STRING_EXTRA}world")

#-----------------------------------------------------------------------------
# Function-style macro command-line support:
#   - Borland does not support
#   - MSVC does not support
#   - Watcom does not support
#   - GCC supports

# Too few platforms support this to bother implementing.
# People can just configure headers with the macros.

#-----------------------------------------------------------------------------
# Construct a sample expression to pass as a macro definition.

set(EXPR "x*y+!(x==(y+1*2))*f(x${EXPR_OP1}2)")

if(NOT WATCOM)
  # Watcom does not support - or / because it parses them as options.
  string(APPEND EXPR " + y/x-x")
endif()

#-----------------------------------------------------------------------------

# Inform the test if the debug configuration is getting built.
string(APPEND CMAKE_C_FLAGS_DEBUG " -DPREPROCESS_DEBUG")
string(APPEND CMAKE_CXX_FLAGS_DEBUG " -DPREPROCESS_DEBUG")
string(APPEND CMAKE_C_FLAGS_RELEASE " -DPREPROCESS_NDEBUG")
string(APPEND CMAKE_CXX_FLAGS_RELEASE " -DPREPROCESS_NDEBUG")
string(APPEND CMAKE_C_FLAGS_RELWITHDEBINFO " -DPREPROCESS_NDEBUG")
string(APPEND CMAKE_CXX_FLAGS_RELWITHDEBINFO " -DPREPROCESS_NDEBUG")
string(APPEND CMAKE_C_FLAGS_MINSIZEREL " -DPREPROCESS_NDEBUG")
string(APPEND CMAKE_CXX_FLAGS_MINSIZEREL " -DPREPROCESS_NDEBUG")

# Inform the test if it built from Xcode.
if(PP_XCODE)
  set(PREPROCESS_XCODE 1)
endif()

# Test old-style definitions.
add_definitions(-DOLD_DEF -DOLD_EXPR=2)

# Make sure old-style definitions are converted to directory property.
set(OLD_DEFS_EXPECTED "OLD_DEF;OLD_EXPR=2")
get_property(OLD_DEFS DIRECTORY PROPERTY COMPILE_DEFINITIONS)
if(NOT "${OLD_DEFS}" STREQUAL "${OLD_DEFS_EXPECTED}")
  message(SEND_ERROR "add_definitions not converted to directory property!")
endif()

add_executable(Preprocess preprocess.c preprocess.cxx)

set(FILE_PATH "${Preprocess_SOURCE_DIR}/file_def.h")
set(TARGET_PATH "${Preprocess_SOURCE_DIR}/target_def.h")

# Set some definition properties.
foreach(c "" "_DEBUG" "_RELEASE" "_RELWITHDEBINFO" "_MINSIZEREL")
  set(FLAVOR "${c}")
  # Treat RelWithDebInfo and MinSizeRel as Release to avoid having
  # an exponentional matrix of inclusions and exclusions of defines
  if("${c}" STREQUAL "_RELWITHDEBINFO" OR "${c}" STREQUAL "_MINSIZEREL")
    set(FLAVOR "_RELEASE")
  endif()
  set_property(
    DIRECTORY .
    APPEND PROPERTY COMPILE_DEFINITIONS${c} "DIRECTORY_DEF${FLAVOR}"
    )
  set_property(
    TARGET Preprocess
    PROPERTY COMPILE_DEFINITIONS${c} "TARGET_DEF${FLAVOR}"
    )
  set_property(
    SOURCE preprocess.c preprocess.cxx
    PROPERTY COMPILE_DEFINITIONS${c} "FILE_DEF${FLAVOR}"
    )
endforeach()

# Add definitions with values.
set(DEF_TARGET_PATH "TARGET_PATH=\"${TARGET_PATH}\"")
set(DEF_FILE_PATH "FILE_PATH=\"${FILE_PATH}\"")
set_property(
  TARGET Preprocess
  APPEND PROPERTY COMPILE_DEFINITIONS
  "TARGET_STRING=\"${STRING_VALUE}${SEMICOLON}\""
  "TARGET_EXPR=${EXPR}"
  ${DEF_TARGET_PATH}
  )
set_property(
  SOURCE preprocess.c preprocess.cxx
  APPEND PROPERTY COMPILE_DEFINITIONS
  "FILE_STRING=\"${STRING_VALUE}${SEMICOLON}\""
  "FILE_EXPR=${EXPR}"
  ${DEF_FILE_PATH}
  )

# Try reading and writing the property value to ensure the string is
# preserved.
get_property(defs1 TARGET Preprocess PROPERTY COMPILE_DEFINITIONS)
set_property(TARGET Preprocess PROPERTY COMPILE_DEFINITIONS "${defs1}")
get_property(defs2 TARGET Preprocess PROPERTY COMPILE_DEFINITIONS)
if(NOT "x${defs1}" STREQUAL "x${defs2}")
  message(FATAL_ERROR "get/set/get COMPILE_DEFINITIONS round trip failed.  "
    "First get:\n"
    "  ${defs1}\n"
    "Second get:\n"
    "  ${defs2}")
endif()

# Helper target for running test manually in build tree.
add_custom_target(drive COMMAND Preprocess)

# Configure the header file with the desired string value.
if(SEMICOLON)
  string(APPEND STRING_VALUE ";")
endif()
configure_file(${Preprocess_SOURCE_DIR}/preprocess.h.in
               ${Preprocess_BINARY_DIR}/preprocess.h)
include_directories(${Preprocess_BINARY_DIR})
